/*
 * Copyright 2010, 2011, 2012 mapsforge.org
 *
 * This program is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.mapsforge.android.maps.overlay;

import org.mapsforge.android.maps.MapView;
import org.mapsforge.core.model.BoundingBox;
import org.mapsforge.core.model.GeoPoint;
import org.mapsforge.core.model.Point;
import org.mapsforge.core.util.MercatorProjection;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.drawable.Drawable;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;

/**
 * A thread-safe {@link Overlay} implementation to display a {@link Circle} and a {@link Drawable} at the user's current
 * location.
 */
public class MyLocationOverlay implements LocationListener, Overlay {
	private static final int UPDATE_DISTANCE = 0;
	private static final int UPDATE_INTERVAL = 1000;

	/**
	 * @param location
	 *            the location whose geographical coordinates should be converted.
	 * @return a new GeoPoint with the geographical coordinates taken from the given location.
	 */
	public static GeoPoint locationToGeoPoint(Location location) {
		return new GeoPoint(location.getLatitude(), location.getLongitude());
	}

	private static Paint getDefaultCircleFill() {
		return getPaint(Style.FILL, Color.BLUE, 48);
	}

	private static Paint getDefaultCircleStroke() {
		Paint paint = getPaint(Style.STROKE, Color.BLUE, 128);
		paint.setStrokeWidth(2);
		return paint;
	}

	private static Paint getPaint(Style style, int color, int alpha) {
		Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
		paint.setStyle(style);
		paint.setColor(color);
		paint.setAlpha(alpha);
		return paint;
	}

	private boolean centerAtNextFix;
	private final Circle circle;
	private Location lastLocation;
	private final LocationManager locationManager;
	private final MapView mapView;
	private final Marker marker;
	private boolean myLocationEnabled;
	private boolean snapToLocationEnabled;

	/**
	 * Constructs a new {@code MyLocationOverlay} with the given drawable and the default circle paints.
	 * 
	 * @param context
	 *            a reference to the application context.
	 * @param mapView
	 *            the {@code MapView} on which the location will be displayed.
	 * @param drawable
	 *            a drawable to display at the current location (might be null).
	 */
	public MyLocationOverlay(Context context, MapView mapView, Drawable drawable) {
		this(context, mapView, drawable, getDefaultCircleFill(), getDefaultCircleStroke());
	}

	/**
	 * Constructs a new {@code MyLocationOverlay} with the given drawable and circle paints.
	 * 
	 * @param context
	 *            a reference to the application context.
	 * @param mapView
	 *            the {@code MapView} on which the location will be displayed.
	 * @param drawable
	 *            a drawable to display at the current location (might be null).
	 * @param circleFill
	 *            the {@code Paint} used to fill the circle that represents the current location (might be null).
	 * @param circleStroke
	 *            the {@code Paint} used to stroke the circle that represents the current location (might be null).
	 */
	public MyLocationOverlay(Context context, MapView mapView, Drawable drawable, Paint circleFill, Paint circleStroke) {
		this.mapView = mapView;
		this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
		this.marker = new Marker(null, drawable);
		this.circle = new Circle(null, 0, circleFill, circleStroke);
	}

	/**
	 * Stops the receiving of location updates. Has no effect if location updates are already disabled.
	 */
	public synchronized void disableMyLocation() {
		if (this.myLocationEnabled) {
			this.myLocationEnabled = false;
			this.locationManager.removeUpdates(this);
			this.mapView.getOverlayController().redrawOverlays();
		}
	}

	@Override
	public synchronized void draw(BoundingBox boundingBox, byte zoomLevel, Canvas canvas) {
		if (!this.myLocationEnabled) {
			return;
		}

		double canvasPixelLeft = MercatorProjection.longitudeToPixelX(boundingBox.minLongitude, zoomLevel);
		double canvasPixelTop = MercatorProjection.latitudeToPixelY(boundingBox.maxLatitude, zoomLevel);
		Point canvasPosition = new Point(canvasPixelLeft, canvasPixelTop);
		this.circle.draw(boundingBox, zoomLevel, canvas, canvasPosition);
		this.marker.draw(boundingBox, zoomLevel, canvas, canvasPosition);
	}

	/**
	 * Enables the receiving of location updates from the most accurate {@link LocationProvider} available.
	 * 
	 * @param centerAtFirstFix
	 *            whether the map should be centered to the first received location fix.
	 * @return true if at least one location provider was found, false otherwise.
	 */
	public synchronized boolean enableMyLocation(boolean centerAtFirstFix) {
		if (!enableBestAvailableProvider()) {
			return false;
		}

		this.centerAtNextFix = centerAtFirstFix;
		return true;
	}

	/**
	 * @return the most-recently received location fix (might be null).
	 */
	public synchronized Location getLastLocation() {
		return this.lastLocation;
	}

	/**
	 * @return true if the map will be centered at the next received location fix, false otherwise.
	 */
	public synchronized boolean isCenterAtNextFix() {
		return this.centerAtNextFix;
	}

	/**
	 * @return true if the receiving of location updates is currently enabled, false otherwise.
	 */
	public synchronized boolean isMyLocationEnabled() {
		return this.myLocationEnabled;
	}

	/**
	 * @return true if the snap-to-location mode is enabled, false otherwise.
	 */
	public synchronized boolean isSnapToLocationEnabled() {
		return this.snapToLocationEnabled;
	}

	@Override
	public void onLocationChanged(Location location) {
		synchronized (this) {
			this.lastLocation = location;

			GeoPoint geoPoint = locationToGeoPoint(location);
			this.marker.setGeoPoint(geoPoint);
			this.circle.setGeoPoint(geoPoint);
			this.circle.setRadius(location.getAccuracy());

			if (this.centerAtNextFix || this.snapToLocationEnabled) {
				this.centerAtNextFix = false;
				this.mapView.getMapViewPosition().setCenter(geoPoint);
			}
		}
		this.mapView.getOverlayController().redrawOverlays();
	}

	@Override
	public void onProviderDisabled(String provider) {
		enableBestAvailableProvider();
	}

	@Override
	public void onProviderEnabled(String provider) {
		enableBestAvailableProvider();
	}

	@Override
	public void onStatusChanged(String provider, int status, Bundle extras) {
		// do nothing
	}

	/**
	 * @param snapToLocationEnabled
	 *            whether the map should be centered at each received location fix.
	 */
	public synchronized void setSnapToLocationEnabled(boolean snapToLocationEnabled) {
		this.snapToLocationEnabled = snapToLocationEnabled;
	}

	private synchronized boolean enableBestAvailableProvider() {
		disableMyLocation();

		Criteria criteria = new Criteria();
		criteria.setAccuracy(Criteria.ACCURACY_FINE);
		String bestAvailableProvider = this.locationManager.getBestProvider(criteria, true);
		if (bestAvailableProvider == null) {
			return false;
		}

		this.locationManager.requestLocationUpdates(bestAvailableProvider, UPDATE_INTERVAL, UPDATE_DISTANCE, this);
		this.myLocationEnabled = true;
		return true;
	}
}
